//! Create documentation pages for each rule. Pages are printed as Markdown and
//! get added to the website.

use std::{
    fmt::{self, Write},
    path::PathBuf,
};

use oxc_linter::{LintPlugins, table::RuleTableRow};
use schemars::{
    JsonSchema, SchemaGenerator,
    schema::{InstanceType, Schema, SchemaObject, SingleOrVec},
};

use crate::linter::json_schema::{self, Renderer};

use super::HtmlWriter;

#[derive(Debug)]
pub(super) struct Context {
    page: HtmlWriter,
    schemas: SchemaGenerator,
    renderer: json_schema::Renderer,
}

impl Context {
    pub fn new<T: JsonSchema + ?Sized>(mut schemas: SchemaGenerator) -> Self {
        let renderer = Renderer::new(schemas.root_schema_for::<T>());
        Self { page: HtmlWriter::with_capacity(1024), schemas, renderer }
    }

    pub fn render_rule_docs_page(&mut self, rule: &RuleTableRow) -> Result<String, fmt::Error> {
        const APPROX_FIX_CATEGORY_AND_PLUGIN_LEN: usize = 512;
        let RuleTableRow {
            name,
            documentation,
            schema,
            plugin,
            turned_on_by_default,
            autofix,
            category,
        } = rule;
        let resolved =
            schema.as_ref().map(|schema| self.schemas.dereference(schema).unwrap_or(schema));

        self.page.reserve(
            documentation.map_or(0, str::len) + name.len() + APPROX_FIX_CATEGORY_AND_PLUGIN_LEN,
        );

        writeln!(
            self.page,
            "<!-- This file is auto-generated by {}. Do not edit it manually. -->\n",
            file!()
        )?;

        writeln!(
            self.page,
            "<script setup>
import {{ data }} from '../version.data.js';
const source = `{}`;
</script>",
            rule_source(rule)
        )?;

        writeln!(self.page, r#"# {plugin}/{name} <Badge type="info" text="{category}" />"#)?;

        // rule metadata
        self.page.div(r#"class="rule-meta""#, |p| {
            if *turned_on_by_default {
                p.Alert(r#"class="default-on" type="success""#, |p| {
                    p.writeln(r#"<span class="emoji">✅</span> This rule is turned on by default."#)
                })?;
            }

            if let Some(emoji) = autofix.emoji() {
                p.Alert(r#"class="fix" type="info""#, |p| {
                    p.writeln(format!(
                        r#"<span class="emoji">{}</span> {}"#,
                        emoji,
                        autofix.description()
                    ))
                })?;
            }

            Ok(())
        })?;

        // rule documentation
        if let Some(docs) = documentation {
            writeln!(self.page, "\n{}", *docs)?;
        }

        // rule configuration
        if let Some(Schema::Object(schema)) = resolved {
            let config_section = self.rule_config(schema);
            if !config_section.trim().is_empty() {
                writeln!(self.page, "\n## Configuration\n{config_section}")?;
            }
        }

        // how to use
        writeln!(self.page, "\n## How to use\n{}", how_to_use(rule))?;
        writeln!(self.page, "\n## References\n")?;
        writeln!(
            self.page,
            r#"- <a v-bind:href="source" target="_blank" rel="noreferrer">Rule Source</a>"#
        )?;

        Ok(self.page.take())
    }

    fn rule_config(&self, schema: &SchemaObject) -> String {
        let mut section = self.renderer.render_schema(2, "", schema);
        if section.default.is_none() {
            if let Some(SingleOrVec::Single(ty)) = &schema.instance_type {
                match &**ty {
                    InstanceType::Boolean => {
                        section.default = Some(format!("{}", <bool>::default()));
                    }
                    InstanceType::Array => {
                        section.default = Some("[]".to_string());
                    }
                    _ => {}
                }
            }
        }
        let mut rendered = section.to_md(&self.renderer);
        if rendered.trim().is_empty() {
            return rendered;
        }

        if schema.instance_type.as_ref().is_some_and(|it| it.contains(&InstanceType::Object)) {
            rendered = format!(
                "\nThis rule accepts a configuration object with the following properties:\n{rendered}\n"
            );
        }

        rendered
    }
}

fn rule_source(rule: &RuleTableRow) -> String {
    use project_root::get_project_root;
    use std::sync::OnceLock;
    const LINT_RULES_DIR: &str = "crates/oxc_linter/src/rules";
    static ROOT: OnceLock<PathBuf> = OnceLock::new();
    let root = ROOT.get_or_init(|| get_project_root().unwrap());

    // Some rules are folders with a mod.rs file, others are just a rust file
    let rule_name = rule.name.replace('-', "_");
    let mut rule_path = format!("{}/{}", rule.plugin, rule_name);
    if root.join(LINT_RULES_DIR).join(&rule.plugin).join(rule_name).is_dir() {
        rule_path.push_str("/mod.rs");
    } else {
        rule_path.push_str(".rs");
    }

    format!(
        "https://github.com/oxc-project/oxc/blob/${{ data }}/crates/oxc_linter/src/rules/{rule_path}"
    )
}

/// Returns `true` if the given plugin is a default plugin.
/// - Example: `eslint` => true
/// - Example: `jest` => false
fn is_default_plugin(plugin: &str) -> bool {
    let plugin = LintPlugins::from(plugin);
    LintPlugins::default().contains(plugin)
}

/// Returns the normalized plugin name.
/// - Example: `react_perf` -> `react-perf`
/// - Example: `eslint` -> `eslint`
/// - Example: `jsx_a11y` -> `jsx-a11y`
fn get_normalized_plugin_name(plugin: &str) -> &str {
    LintPlugins::from(plugin).into()
}

fn how_to_use(rule: &RuleTableRow) -> String {
    let plugin = &rule.plugin;
    let normalized_plugin_name = get_normalized_plugin_name(plugin);
    let rule_full_name = if normalized_plugin_name.is_empty() {
        rule.name.to_string()
    } else {
        format!("{}/{}", normalized_plugin_name, rule.name)
    };
    let is_default_plugin = is_default_plugin(plugin);
    let enable_bash_example = if is_default_plugin {
        format!(r"oxlint --deny {rule_full_name}")
    } else {
        format!(r"oxlint --deny {rule_full_name} --{normalized_plugin_name}-plugin")
    };
    let enable_config_example = if is_default_plugin {
        format!(
            r#"{{
    "rules": {{
        "{rule_full_name}": "error"
    }}
}}"#
        )
    } else {
        format!(
            r#"{{
    "plugins": ["{normalized_plugin_name}"],
    "rules": {{
        "{rule_full_name}": "error"
    }}
}}"#
        )
    };
    format!(
        r"
To **enable** this rule in the CLI or using the config file, you can use:

::: code-group

```bash [CLI]
{enable_bash_example}
```

```json [Config (.oxlintrc.json)]
{enable_config_example}
```

:::
"
    )
}
